#include "multicast_acceptor.hpp"
#include <asio.hpp>
#include <iostream>
#include <utility>
#include <vector>

#include "result.hpp"

class MulticastAcceptor::Impl
{
public:
    explicit Impl(Callback callback)
        : callback(std::move(callback))
    {
    }

    cpp::Result<> init(uint16_t listen_port, const std::string& multicast_address,
                       const std::string& interface_address = "0.0.0.0")
    {
        if (sp_socket)
        {
            return cpp::Result<>::Err("Sock is init.");
        }

        address_multicast = asio::ip::make_address_v4(multicast_address);
        address_interface = asio::ip::make_address_v4(interface_address);

        sp_socket = std::make_shared<asio::ip::udp::socket>(io_context);
        asio::error_code ec;
        if (sp_socket->open(asio::ip::udp::v4(), ec))
        {
            return cpp::Result<>::Err(ec.message());
        }

        if (sp_socket->set_option(asio::ip::udp::socket::reuse_address(true), ec))
        {
            return cpp::Result<>::Err(ec.message());
        }
        if (sp_socket->bind(asio::ip::udp::endpoint(asio::ip::address_v4::any(), listen_port), ec))
        {
            return cpp::Result<>::Err(ec.message());
        }
        asio::ip::multicast::join_group request{address_multicast, address_interface};
        if (sp_socket->set_option(request, ec))
        {
            return cpp::Result<>::Err(ec.message());
        }
        do_receive();
        return cpp::Result<>::Ok();
    }

    void run()
    {
        io_context.run();
    }

    cpp::Result<> release()
    {
        if (sp_socket == nullptr)
        {
            return cpp::Result<>::Err("Sock is no init.");
        }
        if (io_context.stopped() == false)
        {
            io_context.stop();
        }
        asio::error_code ec;
        asio::ip::multicast::leave_group request{
            address_multicast,
            address_interface
        };
        if (sp_socket->set_option(request, ec))
        {
            return cpp::Result<>::Err(ec.message());
        }

        if (sp_socket->close(ec))
        {
            return cpp::Result<>::Err(ec.message());
        }
        sp_socket = nullptr;
        return cpp::Result<>::Ok();
    }

private:
    void do_receive()
    {
        sp_socket->async_receive_from(
            asio::buffer(data_), sender_endpoint_,
            [this](const std::error_code& ec, const std::size_t length)
            {
                if (!ec)
                {
                    if (callback)
                    {
                        const std::vector<uint8_t> received_data(data_.begin(),
                                                                 data_.begin() + static_cast<int32_t>(length));
                        callback(received_data, sender_endpoint_.address().to_string(), sender_endpoint_.port());
                    }
                    do_receive();
                }
            });
    }

    const Callback callback;
    asio::ip::address_v4 address_multicast;
    asio::ip::address_v4 address_interface;
    asio::io_context io_context;
    std::shared_ptr<asio::ip::udp::socket> sp_socket;
    asio::ip::udp::endpoint sender_endpoint_;
    std::array<char, 1024> data_{};
};

MulticastAcceptor::MulticastAcceptor(const Callback& callback)
{
    pimpl = std::make_unique<Impl>(callback);
}

MulticastAcceptor::~MulticastAcceptor() = default;

cpp::Result<> MulticastAcceptor::init(uint16_t listen_port, const std::string& multicast_address,
                             const std::string& interface_address) const
{
    return pimpl->init(listen_port, multicast_address, interface_address);
}

void MulticastAcceptor::run() const
{
    pimpl->run();
}

cpp::Result<> MulticastAcceptor::release() const
{
    return pimpl->release();
}
